# File: NPCS.py
# Copyright 2010 
# Author: Andrew Dorrycott
# License: GNU Lesser General Public License
# Original Repository: http://code.google.com/p/bobthezombie/

import sys
import pygame
import random
import datetime
import math

from Standard import *

class NPC(WorldObject):
    def __init__(self, x, y, engine, view_image = IMAGES_BOB['FaceSouth'], collision_image = IMAGES_BOB['CollisionMask'], image_collection = IMAGES_BOB, scale = 2.0, current_weapon = 'Hands'):
        WorldObject.__init__(self, x, y, engine, view_image, collision_image, scale)
        
        # NPC's need to know their current location and scale when doing animations
        self.move_animations = {}
        self.firststep = 1
        self.has_moved = False
        self.walk_in_directions = [] # 'FaceNorth' or 'FaceSouth' or 'FaceWest' or 'FaceEast' # Only options
        self.last_direction = 'FaceNorth' or 'FaceSouth' or 'FaceWest' or 'FaceEast' # Only options
        
        # How we know if we're Bob or not in the grand scheme of NPC inheritence :D
        self.is_bob = False
        
        # What the player will notice
        self.is_infected = False
        self.is_zombie = False # When converted they can use the 'Infection' weapon
        self.max_health = 100.0
        self.health = 100 # All npcs need health! :P
        self.has_fired = False
        self.current_weapon = current_weapon # What is currently the weapon of choice
        self.current_outfit = None and 'Labcoat' # Disguise Options
        self.has_weapons = ['Hands', "Shotgun", "Pistol", "Rifle"] # This is the inventory of weapons in hand
        self.current_ammo = {'Shotgun': 0,
                             'Pistol': 0,
                             'Rifle': 0,
                             'Hands': 0,} # Infection Weapon, like a zombie using their teeth to eat you :3
        
        # NPC specials
        self.ai_decided_direction = {}
        
        for direction in image_collection:
            image = pygame.image.load(image_collection[direction])
            image = pygame.transform.scale(image, (int( image.get_size()[0] * scale ), int( image.get_size()[1] * scale )))
            
            rectangle = image.get_rect()
            rectangle.x = self.current_x - (rectangle.width / 2.0)
            rectangle.y = self.current_y - (rectangle.height / 2.0)
            
            self.move_animations.setdefault(direction, [image, rectangle])
            
        # Remove unneeded variables from memory
        del image
        del rectangle
    
    def SwitchWeaponTo(self, weapon):
        if weapon in self.has_weapons:
            self.current_weapon = weapon
    
    def Redraw(self, screen):
        WorldObject.Redraw(self, screen)
        
        if self.engine.world_location_debug == True and self.is_bob:
            screen.blit( self.engine.font.render(str(self.health), 0, (255, 255, 255)), ( self.current_x, self.current_y ) )
            screen.blit( self.engine.font.render("(%d,%d)" % (self.world_x, self.world_y), 0, (255, 255, 255)), ( self.current_x - 45, self.current_y - 10 ) )
    
    def UpdateHealth(self, value):
        if self.health + value > self.max_health:
            self.health = self.max_health
        else:
            self.health += value
        
        if self.CanConvert():
            self.engine.SendEvent(CustomEvents.converted, object = self)
        
        elif self.health <= 0:
            self.engine.SendEvent(CustomEvents.died, object = self, is_bob = self.is_bob)
    
    def CanConvert(self):
        if self.is_bob or self.is_zombie: # Bob and Zombies cannot convert to more zombies, that would be cheating :P
            return False
        
        # The closer to death, the higher chance to be lower than the required conversion ratio bob has to become a zombie
        if random.random() - ( 1 - self.health / self.max_health ) < (self.engine.bob.conversion_ratio / 100.0) and \
            self.is_infected:
            return True
        
        return False
    
    def DisplayAnimation(self, animation):
        self.view_image = self.move_animations[animation][0]
        self.view_rectangle = self.move_animations[animation][1]
        
        # Update the image with the current location
        self.view_rectangle.x = self.current_x - ( self.view_image.get_size()[0] / 2.0 )
        self.view_rectangle.y = self.current_y - ( self.view_image.get_size()[1] / 2.0 )
    
    def DisplayDirectionalAnimation(self, direction, step1, step2):
        self.has_moved = True
        if direction not in self.walk_in_directions:
            self.walk_in_directions.append( direction )
        
        self.last_direction = direction
        
        if self.firststep == 16:
            self.DisplayAnimation(step1)
            
        elif self.firststep == 32:
            self.firststep = 0
            
        elif self.firststep == 1:
            self.DisplayAnimation(step2)
        
        self.firststep += 1
    
    def MoveSouth(self, speed):
        if 'FaceNorth' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceEast' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceSouth', 'WalkSouthEastStep1', 'WalkSouthEastStep2')
        elif 'FaceWest' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceSouth', 'WalkSouthWestStep1', 'WalkSouthWestStep2')
        else:
            self.DisplayDirectionalAnimation('FaceSouth', 'WalkSouthStep1', 'WalkSouthStep2')
        
        self.ShiftSouth(speed * -1)
    
    def MoveNorth(self, speed):
        if 'FaceSouth' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceEast' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceNorth', 'WalkNorthEastStep1', 'WalkNorthEastStep2')
        elif 'FaceWest' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceNorth', 'WalkNorthWestStep1', 'WalkNorthWestStep2')
        else:
            self.DisplayDirectionalAnimation('FaceNorth', 'WalkNorthStep1', 'WalkNorthStep2')
        
        self.ShiftNorth(speed * -1)
    
    def MoveWest(self, speed):
        if 'FaceEast' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceNorth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceWest', 'WalkNorthWestStep1', 'WalkNorthWestStep2')
        elif 'FaceSouth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceWest', 'WalkSouthWestStep1', 'WalkSouthWestStep2')
        else:
            self.DisplayDirectionalAnimation('FaceWest', 'WalkWestStep1', 'WalkWestStep2')
        
        self.ShiftWest(speed * -1)
    
    def MoveEast(self, speed):
        if 'FaceWest' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceNorth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceEast', 'WalkNorthEastStep1', 'WalkNorthEastStep2')
        elif 'FaceSouth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceEast', 'WalkSouthEastStep1', 'WalkSouthEastStep2')
        else:
            self.DisplayDirectionalAnimation('FaceEast', 'WalkEastStep1', 'WalkEastStep2')
        
        self.ShiftEast(speed * -1)
    
    def StopMovingSouth(self):
        if 'FaceNorth' in self.walk_in_directions:
            return # Do not attempt to remove empty direction
        
        try:
            direction = self.walk_in_directions.pop( self.walk_in_directions.index('FaceSouth') )
            self.DisplayAnimation(direction)
            del direction
        except ValueError:
            return
    
    def StopMovingNorth(self):
        if 'FaceSouth' in self.walk_in_directions:
            return # Do not attempt to remove empty direction
        
        try:
            direction = self.walk_in_directions.pop( self.walk_in_directions.index('FaceNorth') )
            self.DisplayAnimation(direction)
            del direction
        except ValueError:
            return
    
    def StopMovingWest(self):
        if 'FaceEast' in self.walk_in_directions:
            return # Do not attempt to remove empty direction
        
        try:
            direction = self.walk_in_directions.pop( self.walk_in_directions.index('FaceWest') )
            
            if ('FaceNorth' not in self.walk_in_directions) and ('FaceSouth' not in self.walk_in_directions):
                self.DisplayAnimation(direction)
            
            del direction
        except ValueError:
            return
    
    def StopMovingEast(self):
        if 'FaceWest' in self.walk_in_directions:
            return # Do not attempt to remove empty direction
        
        try:
            direction = self.walk_in_directions.pop( self.walk_in_directions.index('FaceEast') )
            
            if ('FaceNorth' not in self.walk_in_directions) and ('FaceSouth' not in self.walk_in_directions):
                self.DisplayAnimation(direction)
            
            del direction
        except ValueError:
            return
    
    def ShiftSouth(self, distance):
        WorldObject.MoveSouth(self, distance)
    
    def ShiftNorth(self, distance):
        WorldObject.MoveNorth(self, distance)
    
    def ShiftWest(self, distance):
        WorldObject.MoveWest(self, distance)
    
    def ShiftEast(self, distance):
        WorldObject.MoveEast(self, distance)
    
    def StopMovingAnimation(self):
        if not self.has_moved and self.walk_in_directions:
            self.DisplayAnimation(self.walk_in_directions[-1])
        else:
            self.has_moved = False
    
    def FireWeapon(self):
        self.has_fired = True
    
    def CanFire(self):
        #return not self.has_fired and self.current_ammo[self.current_weapon] > 0
        return True # npcs don't need to worry about self control :P
    
    def AI(self):
        if self.ai_decided_direction.keys():
            self.ai_decided_direction['direction']( MOVEMENT_SPEED )
            
            if self.WillCollideWithBoundariesAt():
                self.ai_decided_direction['direction']( MOVEMENT_SPEED * -1 )
                
            self.ai_decided_direction['duration'] -= 1
        
            if self.ai_decided_direction['duration'] <= 0:
                self.ai_decided_direction = {} # Reset the walking direction
        
        # Should the NPC move?
        if self.ai_decided_direction.keys() == [] and random.random() * 100 < 2.5:
            
            direction = random.random() * 100
            
            if direction < 25:
                self.ai_decided_direction.setdefault('direction', self.MoveNorth)
                self.ai_decided_direction.setdefault('duration', 25 * random.random())
                
            elif direction >= 25 and direction < 50:
                self.ai_decided_direction.setdefault('direction', self.MoveSouth)
                self.ai_decided_direction.setdefault('duration', 25 * random.random())
                    
            elif direction >= 50 and direction < 75:
                self.ai_decided_direction.setdefault('direction', self.MoveEast)
                self.ai_decided_direction.setdefault('duration', 25 * random.random())
                    
            elif direction >= 75:
                self.ai_decided_direction.setdefault('direction', self.MoveWest)
                self.ai_decided_direction.setdefault('duration', 25 * random.random())
    
    def WillCollideWithBoundariesAt(self):
        # Find out if the object is running into Bob or the World
        return pygame.sprite.collide_mask(self.engine.bob, self) or \
            pygame.sprite.collide_mask(self.engine.world, self) or \
            self.engine.IsCollidingWithOtherNPCs(self)
        
        # This is quite interesting, the Super class has knowledge of Bob despite this class coming before Bob :)

class Intern(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_INTERN['FaceSouth'], collision_image = IMAGES_INTERN['CollisionMask'], image_collection = IMAGES_INTERN, scale = 2.0, current_weapon = 'Hands'):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale, current_weapon)

class Female(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_FEMALE['FaceSouth'], collision_image = IMAGES_FEMALE['CollisionMask'], image_collection = IMAGES_FEMALE, scale = 1.7, current_weapon = 'Hands'):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale, current_weapon)

class Male(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_MALE['FaceSouth'], collision_image = IMAGES_MALE['CollisionMask'], image_collection = IMAGES_MALE, scale = 1.7, current_weapon = 'Hands'):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale, current_weapon)

class ZombieFemale(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_ZOMBIE_FEMALE['FaceSouth'], collision_image = IMAGES_ZOMBIE_FEMALE['CollisionMask'], image_collection = IMAGES_ZOMBIE_FEMALE, scale = 1.7, current_weapon = 'Hands'):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale, current_weapon)
        self.is_zombie = True

class ZombieMale(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_ZOMBIE_MALE['FaceSouth'], collision_image = IMAGES_ZOMBIE_MALE['CollisionMask'], image_collection = IMAGES_ZOMBIE_MALE, scale = 1.7, current_weapon = 'Hands'):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale, current_weapon)
        self.is_zombie = True

class ZombieIntern(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_ZOMBIE_INTERN['FaceSouth'], collision_image = IMAGES_ZOMBIE_INTERN['CollisionMask'], image_collection = IMAGES_ZOMBIE_INTERN, scale = 2.0, current_weapon = 'Hands'):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale, current_weapon)
        self.is_zombie = True

class Bob(NPC):
    def __init__(self, x, y, engine, view_image = IMAGES_BOB['FaceSouth'], collision_image = IMAGES_BOB['CollisionMask'], image_collection = IMAGES_BOB, scale = 2.0):
        NPC.__init__(self, x, y, engine, view_image, collision_image, image_collection, scale)
        
        # What the player will see
        self.dna = 0
        self.cure = 0
        self.sanity = 200 # For ease of calculating the alpha channel, using 200 has positive 100 sanity, 100 as 0 and 0 as negative 100 sanity
        
        # Tech Levels
        self.tech_levels = {"Stealth": 0, # Extra 5% per level to sneak
                            "Regeneration": 0, # Extra 1 health per second per level
                            "Human Killing Machine": 0, # Extra 2% damage to weapons per level
                            "Zombie Army": 0, # Extra 2% per level to conversion raio of infected human
                            "Research Knowledge": 0, # Unlock the knowledge to use higher level documents
                            }
        
        # Derived from Tech Level
        self.heal_tic = 0
        self.conversion_ratio = 4.0 # For now any NPC attacked with infection that is below 50% will convert
        self.cover_amount = 0 # 0% chance of Bob being hidden in public
        
        # What defines Bob from another NPC
        self.view_evil_image = pygame.image.load( IMAGES_BOB['EvilFaceSouth'] )
        self.view_evil_image.set_alpha(0) # Do not show him visible at first
        self.view_evil_rectangle = self.view_evil_image.get_rect()
        self.is_bob = True
        
        # Special Dev Cheat for now :P
        self.current_ammo = {'Shotgun': 100,
                             'Pistol': 100,
                             'Rifle': 100,
                             'Hands': 0,}
    
    def UpdateTech(self, tech, level):
        if tech not in self.tech_levels.keys():
            raise ValueError("Bob does not have '%s' as a tech skill" % tech)
        
        if level > self.dna:
            return # Unable to upgrade
        
        self.tech_levels[tech] += level
        self.dna -= level
        
        if tech == "Stealth":
            self.cover_amount = self.tech_levels[tech] * 5
        elif tech == "Regeneration":
            self.heal_tic = self.tech_levels[tech]
        elif tech == "Zombie Army":
            self.conversion_ratio = self.tech_levels[tech] * 2 + 4 # Extra 5% per level to raio with base 10%
    
    def UpdateCure(self, amount):
        self.cure += amount
        
        if self.cure >= 100:
            self.engine.SendEvent(CustomEvents.cured, object = self)
    
    def UpdateSanity(self, value):
        if (self.sanity + value > 200):
            self.sanity = 200
            return
        
        if (self.sanity + value < 0):
            self.sanity = 0
            return
        
        self.sanity += value
        
        # Update the evil bob's alpha
        percentage = abs(self.sanity / 200.0 - 1)
        self.view_evil_image.set_alpha( int( 255 * percentage ) )
    
    def UpdateDNA(self, value):
        self.dna += value
    
    def UpdateDisguise(self, disguise, cover_amount):
        self.cover_amount = cover_amount
        self.current_outfit = disugise
    
    def UpdateAmmo(self, weapon, amount):
        if weapon != 'Hands':
            self.current_ammo[weapon] += amount
    
    def AddWeapon(self, weapon):
        if weapon not in self.has_weapons:
            self.has_weapons.append(weapon)
    
    def CanConvert(self):
        return False # Bob can never convert :P

    ###################### Over loaded from NPC
    def Redraw(self, screen):
        NPC.Redraw(self, screen)
        
        screen.blit(self.view_evil_image, self.view_evil_rectangle) # Evil Bob :D
    
    def DisplayAnimation(self, animation):
        self.view_image = self.move_animations[animation][0]
        self.view_rectangle = self.move_animations[animation][1]
        
        # This is for evil Bob
        self.view_evil_image = self.move_animations["Evil" + animation][0]
        self.view_evil_rectangle = self.move_animations["Evil" + animation][1]
        
        self.UpdateSanity(0) # Update the new evil bob image with the current alpha

    def ShiftSouth(self, speed):
        self.view_rectangle = self.view_rectangle.move( [0, speed] )
        self.rect = self.rect.move( [0, speed] )
    
    def ShiftNorth(self, speed):
        self.view_rectangle = self.view_rectangle.move( [0, speed * -1] )
        self.rect = self.rect.move( [0, speed * -1] )
    
    def ShiftWest(self, speed):
        self.view_rectangle = self.view_rectangle.move( [speed * -1, 0] )
        self.rect = self.rect.move( [speed * -1, 0] )
    
    def ShiftEast(self, speed):
        self.view_rectangle = self.view_rectangle.move( [speed, 0] )
        self.rect = self.rect.move( [speed, 0] )
    
    def MoveSouth(self, speed):
        if 'FaceNorth' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceEast' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceSouth', 'WalkSouthEastStep1', 'WalkSouthEastStep2')
        elif 'FaceWest' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceSouth', 'WalkSouthWestStep1', 'WalkSouthWestStep2')
        else:
            self.DisplayDirectionalAnimation('FaceSouth', 'WalkSouthStep1', 'WalkSouthStep2')
        
        self.ShiftSouth( speed )
        ran_into_something = self.WillCollideWithBoundariesAt()
        
        self.ShiftSouth( speed * -1 ) # Go Back to original position
        
        if ran_into_something:
            return False # Cannot Move
        
        self.world_y += speed
        return True # Can Move
    
    def MoveNorth(self, speed):
        if 'FaceSouth' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceEast' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceNorth', 'WalkNorthEastStep1', 'WalkNorthEastStep2')
        elif 'FaceWest' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceNorth', 'WalkNorthWestStep1', 'WalkNorthWestStep2')
        else:
            self.DisplayDirectionalAnimation('FaceNorth', 'WalkNorthStep1', 'WalkNorthStep2')
        
        self.ShiftNorth( speed )
        ran_into_something = self.WillCollideWithBoundariesAt()
        self.ShiftNorth( speed * -1 ) # Go Back to original position
        
        if ran_into_something:
            return False # Cannot Move
        
        self.world_y -= speed
        return True # Can Move
    
    def MoveWest(self, speed):
        if 'FaceEast' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceNorth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceWest', 'WalkNorthWestStep1', 'WalkNorthWestStep2')
        elif 'FaceSouth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceWest', 'WalkSouthWestStep1', 'WalkSouthWestStep2')
        else:
            self.DisplayDirectionalAnimation('FaceWest', 'WalkWestStep1', 'WalkWestStep2')
        
        self.ShiftWest( speed )
        ran_into_something = self.WillCollideWithBoundariesAt()
        self.ShiftWest( speed * -1 ) # Go Back to original position
        
        if ran_into_something:
            return False # Cannot Move
        
        self.world_x -= speed
        return True # Can Move
    
    def MoveEast(self, speed):
        if 'FaceWest' in self.walk_in_directions:
            return # Do not attempt to go two directions at once
        
        if 'FaceNorth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceEast', 'WalkNorthEastStep1', 'WalkNorthEastStep2')
        elif 'FaceSouth' in self.walk_in_directions:
            self.DisplayDirectionalAnimation('FaceEast', 'WalkSouthEastStep1', 'WalkSouthEastStep2')
        else:
            self.DisplayDirectionalAnimation('FaceEast', 'WalkEastStep1', 'WalkEastStep2')
        
        self.ShiftEast( speed )
        ran_into_something = self.WillCollideWithBoundariesAt()
        self.ShiftEast( speed * -1 ) # Go Back to original position
        
        if ran_into_something:
            return False # Cannot Move
        
        self.world_x += speed
        return True # Can Move
    
    def FireWeapon(self):
        # Bob has special requirements
        self.UpdateAmmo(self.current_weapon, -1)
        
        self.has_fired = True
        
    def CanFire(self):
        # Bob has special requirements
        if self.current_weapon == 'Hands':
            return True
        
        return not self.has_fired and self.current_ammo[self.current_weapon] > 0
    
    def WillCollideWithBoundariesAt(self):
        # Debugging purposes
        if self.engine.noclip:
            return False
        
        # Find out if the object is running into Bob or the World
        return pygame.sprite.collide_mask(self.engine.world, self) or \
                self.engine.IsCollidingWithOtherNPCs(self)